package org.smallmind.scribe.pen.nutsnbolts.util;

import java.lang.management.ManagementFactory;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.SecureRandom;
import java.util.Arrays;
import org.smallmind.scribe.pen.nutsnbolts.lang.StaticInitializationError;
import org.smallmind.scribe.pen.nutsnbolts.lang.UnknownSwitchCaseException;

public class UniqueId {

   public static enum TYPE {

      BYTE_ARRAY, BIG_INTEGER, COMPACT_STRING, DOTTED_STRING
   }

   public static final int BYTE_ARRAY_LENGTH = 18;

   private static final int[] DOT_OFFSET_0 = {0, 0, 0};
   private static final int[] DOT_OFFSET_1 = {0, 0, 0};
   private static final int[] DOT_OFFSET_2 = {0, 1, 0};
   private static final int[] DOT_OFFSET_3 = {0, 1, 1};

   private static final int CODE_TEMPLATE_BITS = 5;
   private static final String CODE_TEMPLATE = "ABCDEFGHIJKMNPQRSTUVWXYZ23456789";

   private static final int COMPACT_CODE_TEMPLATE_BITS = 6;
   private static final String COMPACT_CODE_TEMPLATE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890$*";

   private static final int[] POWER_ARRAY = {1, 2, 4, 8, 16, 32, 64, 128};

   private static final SecureRandom RANDOM = new SecureRandom(Bytes.getBytes(System.currentTimeMillis()));

   private static final byte[] IP_BYTES = new byte[6];
   private static final byte[] RANDOM_BYTES = new byte[2];

   private static byte[] JVM_TIME_BYTES;
   private static short COUNT = Short.MIN_VALUE;

   static {

      byte[] ipAddress;

      try {
         ipAddress = InetAddress.getLocalHost().getAddress();
         System.arraycopy(ipAddress, 0, IP_BYTES, 6 - ipAddress.length, ipAddress.length);
      }
      catch (UnknownHostException unknownHostException) {
         throw new StaticInitializationError(unknownHostException);
      }

      JVM_TIME_BYTES = Bytes.getBytes(ManagementFactory.getRuntimeMXBean().getStartTime());
      RANDOM.nextBytes(RANDOM_BYTES);
   }

   public synchronized static byte[] generateByteArray () {

      byte[] uniqueId = new byte[18];

      if (COUNT == Short.MAX_VALUE) {
         JVM_TIME_BYTES = Bytes.getBytes(System.currentTimeMillis());
         COUNT = Short.MIN_VALUE;
      }

      System.arraycopy(IP_BYTES, 0, uniqueId, 0, 6);
      System.arraycopy(JVM_TIME_BYTES, 0, uniqueId, 6, 8);
      System.arraycopy(RANDOM_BYTES, 0, uniqueId, 14, 2);
      System.arraycopy(Bytes.getBytes(COUNT++), 0, uniqueId, 16, 2);

      return uniqueId;
   }

   public static BigInteger generateBigInteger () {

      return new BigInteger(generateByteArray());
   }

   public static String generateCompactString () {

      return generateTemplateString(COMPACT_CODE_TEMPLATE, COMPACT_CODE_TEMPLATE_BITS).toString();
   }

   public static String generateDottedString () {

      StringBuilder dottedIdBuilder = generateTemplateString(CODE_TEMPLATE, CODE_TEMPLATE_BITS);
      int dashSize;

      dashSize = dottedIdBuilder.length() / 4;
      switch (dottedIdBuilder.length() % 4) {
         case 0:
            return insertDots(dottedIdBuilder, DOT_OFFSET_0, dashSize);
         case 1:
            return insertDots(dottedIdBuilder, DOT_OFFSET_1, dashSize);
         case 2:
            return insertDots(dottedIdBuilder, DOT_OFFSET_2, dashSize);
         default:
            return insertDots(dottedIdBuilder, DOT_OFFSET_3, dashSize);
      }
   }

   private static String insertDots (StringBuilder dottedIdBuilder, int[] offsets, int dashSize) {

      for (int count = 0; count < offsets.length; count++) {
         dottedIdBuilder.insert((dashSize * (count + 1)) + count + offsets[count], '.');
      }

      return dottedIdBuilder.toString();
   }

   private static StringBuilder generateTemplateString (String template, int templateBits) {

      StringBuilder uniqueIdBuilder = new StringBuilder();
      byte[] codeArray;
      int codeIndex = 0;
      int codeValue = 0;

      codeArray = generateByteArray();

      for (byte codeByte : codeArray) {
         for (int count = 0; count < 8; count++) {
            if ((codeByte & POWER_ARRAY[count]) != 0) {
               codeValue += POWER_ARRAY[codeIndex];
            }

            if (++codeIndex == templateBits) {
               uniqueIdBuilder.append(template.charAt(codeValue));
               codeIndex = 0;
               codeValue = 0;
            }
         }
      }

      if (codeIndex > 0) {
         uniqueIdBuilder.append(CODE_TEMPLATE.charAt(codeValue));
      }

      return uniqueIdBuilder;
   }

   public static boolean equivalent (UniquelyIdentified obj1, UniquelyIdentified obj2) {

      if (obj1.getType().equals(obj2.getType())) {
         switch (obj1.getType()) {
            case COMPACT_STRING:
               return obj1.getUniqueIdentifier().equals(obj2.getUniqueIdentifier());
            case DOTTED_STRING:
               return obj1.getUniqueIdentifier().equals(obj2.getUniqueIdentifier());
            case BIG_INTEGER:
               return obj1.getUniqueIdentifier().equals(obj2.getUniqueIdentifier());
            case BYTE_ARRAY:
               Arrays.equals((byte[])obj1.getUniqueIdentifier(), (byte[])obj2.getUniqueIdentifier());
            default:
               throw new UnknownSwitchCaseException(obj1.getType().name());
         }
      }

      return false;
   }
}